﻿namespace Rialternative.DescriptionProvider
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data.Objects;
    using System.Linq;
    using System.ServiceModel.DomainServices.EntityFramework;
    using System.ServiceModel.DomainServices.Server;

    /// <summary>
    /// DomainService description generator which automatically add C.R.U.D operations for the entities.
    /// </summary>
    internal class EntityDomainServiceDescriptionProvider : DomainServiceDescriptionProvider
    {
        /// <summary>
        /// The ObjectContext type.
        /// </summary>
        private readonly Type objectContextType;

        /// <summary>
        /// The domain service type.
        /// </summary>
        private readonly Type domainServiceType;

        /// <summary>
        /// The LinqToEntities domain service description provider.
        /// Unable to declare it as a LinqToEntitiesDomainServiceDescriptionProvider type because this type is internal.
        /// </summary>
        private readonly DomainServiceDescriptionProvider linqToEntitiesProvider;

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityDomainServiceDescriptionProvider"/> class.
        /// </summary>
        /// <param name="domainServiceType">Type of the domain service.</param>
        /// <param name="parent">The parent.</param>
        /// <param name="objectContextType">Type of the object context.</param>
        public EntityDomainServiceDescriptionProvider(Type domainServiceType, DomainServiceDescriptionProvider parent, Type objectContextType)
            : base(domainServiceType, parent)
        {
            this.objectContextType = objectContextType;
            this.domainServiceType = domainServiceType;
            this.linqToEntitiesProvider = new LinqToEntitiesDomainServiceDescriptionProviderAttribute(objectContextType).CreateProvider(domainServiceType, parent);
        }

        /// <summary>
        /// Gets the <see cref="T:System.ServiceModel.DomainServices.Server.DomainServiceDescription"/> for the <see cref="T:System.ServiceModel.DomainServices.Server.DomainService"/> Type. Overrides should
        /// call base and either extend the <see cref="T:System.ServiceModel.DomainServices.Server.DomainServiceDescription"/> returned or use it as input in creating
        /// an entirely new <see cref="T:System.ServiceModel.DomainServices.Server.DomainServiceDescription"/>.
        /// </summary>
        /// <returns>
        /// The <see cref="T:System.ServiceModel.DomainServices.Server.DomainServiceDescription"/> for the <see cref="T:System.ServiceModel.DomainServices.Server.DomainService"/> Type.
        /// </returns>
        /// <remarks>
        /// This method can extend the base <see cref="T:System.ServiceModel.DomainServices.Server.DomainServiceDescription"/> with new operations by calling
        /// <see cref="M:System.ServiceModel.DomainServices.Server.DomainServiceDescription.AddOperation(System.ServiceModel.DomainServices.Server.DomainOperationEntry)"/>.
        /// </remarks>
        public override DomainServiceDescription GetDescription()
        {
            var description = this.linqToEntitiesProvider.GetDescription();
            var entities = this.GetEntityDomainFromDomainService();

            entities.ToList()
                    .ForEach(entity => this.AddOperations(description, entity));

            return description;
        }

        /// <summary>
        /// This method is called after the <see cref="T:System.ServiceModel.DomainServices.Server.DomainServiceDescription"/> has been created, and allows
        /// additional metadata to be added to the specified operation. Overrides should call base to get the
        /// initial set of attributes, and any additional attributes should be added to those.
        /// </summary>
        /// <param name="operation">The operation to return attributes for.</param>
        /// <returns>The operation attributes.</returns>
        public override AttributeCollection GetOperationAttributes(DomainOperationEntry operation)
        {
            return this.linqToEntitiesProvider.GetOperationAttributes(operation);
        }

        /// <summary>
        /// Gets the <see cref="T:System.ComponentModel.TypeDescriptor"/> for the specified Type, using the specified parent descriptor
        /// as the base. Overrides should call base to ensure the <see cref="T:System.ComponentModel.TypeDescriptor"/>s are chained properly.
        /// </summary>
        /// <param name="type">The Type to return a descriptor for.</param>
        /// <param name="parent">The parent descriptor.</param>
        /// <returns>
        /// The <see cref="T:System.ComponentModel.TypeDescriptor"/> for the specified Type.
        /// </returns>
        public override ICustomTypeDescriptor GetTypeDescriptor(Type type, ICustomTypeDescriptor parent)
        {
            return this.linqToEntitiesProvider.GetTypeDescriptor(type, parent);
        }

        /// <summary>
        /// Determines if the specified <see cref="T:System.Type"/> should be considered an entity <see cref="T:System.Type"/>.
        /// The base implementation returns <c>false</c>.
        /// </summary>
        /// <param name="type">The <see cref="T:System.Type"/> to check.</param>
        /// <returns>
        /// Returns <c>true</c> if the <see cref="T:System.Type"/> should be considered an entity,
        /// <c>false</c> otherwise.
        /// </returns>
        /// <remarks>Effectively, the return from this method is this provider's vote as to whether the specified
        /// Type is an entity. The votes from this provider and all other providers in the chain are used
        /// by <see cref="M:System.ServiceModel.DomainServices.Server.DomainServiceDescriptionProvider.IsEntityType(System.Type)"/> to make it's determination.</remarks>
        public override bool LookupIsEntityType(Type type)
        {
            return this.linqToEntitiesProvider.LookupIsEntityType(type);
        }

        /// <summary>
        /// Adds the operations.
        /// </summary>
        /// <param name="description">The description.</param>
        /// <param name="entity">The operations to add.</param>
        private void AddOperations(DomainServiceDescription description, EntityDomainAttribute entity)
        {
            Type entityType = entity.EntityType;
            EntityDomainOperations operations = entity.Operations;
            EntityDomainOperations requiresAuthentication = entity.RequiresAuthentication;

            this.AddOperation(description, entityType, operations, requiresAuthentication, DomainOperation.Query, EntityDomainOperations.Query, entity.QueryRequiresRole);
            this.AddOperation(description, entityType, operations, requiresAuthentication, DomainOperation.Insert, EntityDomainOperations.Insert, entity.InsertRequiresRole);
            this.AddOperation(description, entityType, operations, requiresAuthentication, DomainOperation.Update, EntityDomainOperations.Update, entity.UpdateRequiresRole);
            this.AddOperation(description, entityType, operations, requiresAuthentication, DomainOperation.Delete, EntityDomainOperations.Delete, entity.DeleteRequiresRole);
        }

        /// <summary>
        /// Add one operation.
        /// </summary>
        /// <param name="description">The description.</param>
        /// <param name="entityType">The type of entity.</param>
        /// <param name="operations">The full operation set.</param>
        /// <param name="requiresAuthentication">The full RequiresAuthentication set.</param>
        /// <param name="domainOperation">The operation to add.</param>
        /// <param name="operationflag">The flag of operation.</param>
        /// <param name="requiresRole">The requiresRole of operation.</param>
        private void AddOperation(
                        DomainServiceDescription description,
                        Type entityType,
                        EntityDomainOperations operations,
                        EntityDomainOperations requiresAuthentication,
                        DomainOperation domainOperation,
                        EntityDomainOperations operationflag,
                        string[] requiresRole)
        {
            if (operations.HasFlag(operationflag) &&
                EntityDomainOperationEntry.GetSpecificMethod(description.DomainServiceType, entityType, domainOperation) == null)
            {
                List<Attribute> attributes = new List<Attribute>();

                if (requiresAuthentication.HasFlag(operationflag))
                {
                    attributes.Add(new RequiresAuthenticationAttribute());
                }

                if (requiresRole.Length > 0)
                {
                    attributes.Add(new RequiresRoleAttribute(requiresRole));
                }

                description.AddOperation(
                    new EntityDomainOperationEntry(description.DomainServiceType, this.objectContextType, entityType, domainOperation, new AttributeCollection(attributes.ToArray())));
            }
        }

        /// <summary>
        /// Gets the entity domain from domain service.
        /// </summary>
        /// <returns>The entity domain.</returns>
        private IEnumerable<EntityDomainAttribute> GetEntityDomainFromDomainService()
        {
            return (EntityDomainAttribute[])this.domainServiceType.GetCustomAttributes(typeof(EntityDomainAttribute), true);
        }

        /// <summary>
        /// Gets the entity domain from model.
        /// </summary>
        /// <returns>The entity domain.</returns>
        private IEnumerable<EntityDomainAttribute> GetEntityDomainFromModel()
        {
            var entityTypesFromModel =
                from pi in this.objectContextType.GetProperties()
                where pi.PropertyType.IsGenericType &&
                    pi.PropertyType.GetGenericTypeDefinition() == typeof(ObjectSet<>)
                select pi.PropertyType.GetGenericArguments()[0];

            var entityTypesWithMetada =
                from t in entityTypesFromModel
                select new EntityDomainAttribute { EntityType = t, Operations = EntityDomainOperations.All };

            return entityTypesWithMetada;
        }
    }
}
